KS
Killer-Skills

MCP Builder — Categories.community

v1.0.0
GitHub

About this Skill

Perfect for Advanced Development Agents needing comprehensive server development guides and production-ready implementation patterns. The most comprehensive Claude Code skills registry | Web Search: https://skills-registry-web.vercel.app

majiayu000 majiayu000
[0]
[0]
Updated: 2/20/2026

Quality Score

Top 5%
90
Excellent
Based on code quality & docs
Installation
SYS Universal Install (Auto-Detect)
Cursor IDE Windsurf IDE VS Code IDE
> npx killer-skills add majiayu000/claude-skill-registry/MCP Builder

Agent Capability Analysis

The MCP Builder MCP Server by majiayu000 is an open-source Categories.community integration for Claude and other AI agents, enabling seamless task automation and capability expansion.

Ideal Agent Persona

Perfect for Advanced Development Agents needing comprehensive server development guides and production-ready implementation patterns.

Core Value

Empowers agents to design and implement production-ready servers with robust error handling, security best practices, and thorough testing strategies using tool design principles and implementation patterns. It streamlines development with standardized documentation and highlights common pitfalls to avoid, ensuring high-quality server development.

Capabilities Granted for MCP Builder MCP Server

Implementing robust error handling mechanisms for server applications
Designing secure server architectures with security best practices
Developing comprehensive testing strategies for production-ready servers

! Prerequisites & Limits

  • Requires in-depth knowledge of server development principles
  • Focused on production-ready server development, may not be suitable for prototype or proof-of-concept projects
Project
SKILL.md
66.8 KB
.cursorrules
1.2 KB
package.json
240 B
Ready
UTF-8

# Tags

[No tags]
SKILL.md
Readonly

MCP Builder: Production-Ready Server Development Guide

Table of Contents

  1. MCP Fundamentals
  2. Tool Design Principles
  3. Implementation Patterns
  4. Error Handling
  5. Testing Strategy
  6. Security Best Practices
  7. Documentation Standards
  8. Production Readiness
  9. Common Pitfalls
  10. Real-World Examples

MCP Fundamentals

Protocol Architecture

MCP (Model Context Protocol) is a standardized protocol for connecting AI assistants to external data sources and tools. It uses JSON-RPC 2.0 over stdio, HTTP, or SSE transports.

Key Components:

  • Server: Exposes tools, resources, and prompts
  • Client: Claude Desktop or other MCP clients
  • Transport Layer: Communication mechanism (stdio, HTTP, SSE)
  • Protocol: JSON-RPC 2.0 messages

Transport Layers

Stdio (Standard Input/Output)

  • Best for: Local development, CLI tools
  • Pros: Simple, no network setup
  • Cons: Single client, no concurrent connections

HTTP

  • Best for: Web services, remote servers
  • Pros: Scalable, standard protocol
  • Cons: Requires HTTP server setup

SSE (Server-Sent Events)

  • Best for: Real-time updates, streaming
  • Pros: Push updates, efficient
  • Cons: One-way from server to client

When to Use MCP vs Alternatives

Use MCP when:

  • Building tools for Claude Desktop
  • Need standardized tool interface
  • Want protocol-level compatibility
  • Require resource discovery

Consider alternatives when:

  • Building custom integrations
  • Need proprietary protocols
  • Performance is critical (direct API calls)
  • Legacy system integration

Client-Server Communication Patterns

Request-Response Pattern

python
1# FastMCP - Tool execution 2@mcp.tool() 3async def get_user_info(user_id: str) -> dict: 4 """Fetch user information""" 5 return {"id": user_id, "name": "John"}
typescript
1// MCP SDK - Tool execution 2server.setRequestHandler(ListToolsRequestSchema, async () => ({ 3 tools: [{ 4 name: "get_user_info", 5 description: "Fetch user information", 6 inputSchema: { 7 type: "object", 8 properties: { 9 user_id: { type: "string" } 10 } 11 } 12 }] 13}));

Tool Design Principles

Atomic Operations

✅ GOOD: Atomic, Single Responsibility

python
1# FastMCP 2@mcp.tool() 3async def create_issue(title: str, body: str, repo: str) -> dict: 4 """Create a single GitHub issue""" 5 # One clear action 6 pass 7 8@mcp.tool() 9async def update_issue(issue_id: int, title: str = None, body: str = None) -> dict: 10 """Update an existing GitHub issue""" 11 # Separate update operation 12 pass
typescript
1// MCP SDK 2const createIssue = { 3 name: "create_issue", 4 description: "Create a single GitHub issue in a repository", 5 inputSchema: { 6 type: "object", 7 required: ["title", "body", "repo"], 8 properties: { 9 title: { type: "string", description: "Issue title" }, 10 body: { type: "string", description: "Issue body" }, 11 repo: { type: "string", description: "Repository name" } 12 } 13 } 14};

❌ BAD: Multi-purpose, Complex

python
1# BAD: Too many responsibilities 2@mcp.tool() 3async def manage_issue(action: str, **kwargs) -> dict: 4 """Create, update, delete, or list issues""" 5 # Too many operations in one tool 6 pass

Clear Input/Output Contracts

✅ GOOD: Explicit Types and Validation

python
1# FastMCP with Pydantic 2from pydantic import BaseModel, Field, validator 3 4class CreateIssueInput(BaseModel): 5 title: str = Field(..., min_length=1, max_length=200) 6 body: str = Field(..., min_length=1) 7 repo: str = Field(..., pattern=r"^[\w\-\.]+/[\w\-\.]+$") 8 labels: list[str] = Field(default_factory=list, max_items=10) 9 10@mcp.tool() 11async def create_issue(input: CreateIssueInput) -> dict: 12 """Create a GitHub issue with validated inputs""" 13 return { 14 "id": 123, 15 "title": input.title, 16 "url": f"https://github.com/{input.repo}/issues/123" 17 }
typescript
1// MCP SDK with Zod validation 2import { z } from "zod"; 3 4const CreateIssueSchema = z.object({ 5 title: z.string().min(1).max(200), 6 body: z.string().min(1), 7 repo: z.string().regex(/^[\w\-\.]+\/[\w\-\.]+$/), 8 labels: z.array(z.string()).max(10).default([]) 9}); 10 11server.setRequestHandler(CallToolRequestSchema, async (request) => { 12 if (request.params.name === "create_issue") { 13 const input = CreateIssueSchema.parse(request.params.arguments); 14 // Process validated input 15 return { 16 content: [{ 17 type: "text", 18 text: JSON.stringify({ id: 123, title: input.title }) 19 }] 20 }; 21 } 22});

Parameter Validation

✅ GOOD: Comprehensive Validation

python
1# FastMCP 2from typing import Literal 3from pydantic import BaseModel, validator, HttpUrl 4 5class SearchParams(BaseModel): 6 query: str = Field(..., min_length=1, max_length=500) 7 limit: int = Field(default=10, ge=1, le=100) 8 sort: Literal["relevance", "date", "stars"] = "relevance" 9 10 @validator("query") 11 def validate_query(cls, v): 12 if any(char in v for char in ["<", ">", "&"]): 13 raise ValueError("Query contains invalid characters") 14 return v.strip()
typescript
1// MCP SDK 2const SearchSchema = z.object({ 3 query: z.string().min(1).max(500).refine( 4 (val) => !/[<>&]/.test(val), 5 "Query contains invalid characters" 6 ), 7 limit: z.number().int().min(1).max(100).default(10), 8 sort: z.enum(["relevance", "date", "stars"]).default("relevance") 9});

Descriptive Naming Conventions

✅ GOOD: Clear, Action-Oriented Names

python
1# FastMCP 2@mcp.tool() 3async def fetch_repository_details(owner: str, repo_name: str) -> dict: 4 """Fetch detailed information about a GitHub repository""" 5 pass 6 7@mcp.tool() 8async def list_open_pull_requests(owner: str, repo_name: str) -> list[dict]: 9 """List all open pull requests in a repository""" 10 pass

❌ BAD: Vague Names

python
1# BAD: Unclear purpose 2@mcp.tool() 3async def get_data(owner: str, repo: str) -> dict: 4 """Get data""" 5 pass

When to Split vs Combine Tools

Decision Tree:

  1. Different operations? → Split (create vs update vs delete)
  2. Different data sources? → Split (GitHub vs GitLab)
  3. Different authentication? → Split
  4. Same operation, different filters? → Combine with parameters
  5. Related operations in same transaction? → Consider combining

✅ GOOD: Appropriate Splitting

python
1# FastMCP - Separate tools for different operations 2@mcp.tool() 3async def create_document(title: str, content: str) -> dict: 4 """Create a new Notion page""" 5 pass 6 7@mcp.tool() 8async def update_document(page_id: str, title: str = None, content: str = None) -> dict: 9 """Update an existing Notion page""" 10 pass 11 12@mcp.tool() 13async def delete_document(page_id: str) -> dict: 14 """Delete a Notion page""" 15 pass

Implementation Patterns

REST API Integration

FastMCP Example:

python
1import httpx 2from fastmcp import FastMCP 3from pydantic import BaseModel, HttpUrl 4 5mcp = FastMCP("GitHub MCP Server") 6 7class GitHubIssue(BaseModel): 8 title: str 9 body: str 10 labels: list[str] = [] 11 12@mcp.tool() 13async def create_github_issue( 14 owner: str, 15 repo: str, 16 title: str, 17 body: str, 18 token: str 19) -> dict: 20 """Create a GitHub issue""" 21 async with httpx.AsyncClient() as client: 22 response = await client.post( 23 f"https://api.github.com/repos/{owner}/{repo}/issues", 24 json={"title": title, "body": body}, 25 headers={"Authorization": f"token {token}"}, 26 timeout=30.0 27 ) 28 response.raise_for_status() 29 return response.json() 30 31if __name__ == "__main__": 32 mcp.run()

MCP SDK Example:

typescript
1import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 2import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 3import fetch from "node-fetch"; 4 5const server = new Server({ 6 name: "github-mcp-server", 7 version: "1.0.0" 8}, { 9 capabilities: { 10 tools: {} 11 } 12}); 13 14server.setRequestHandler(CallToolRequestSchema, async (request) => { 15 if (request.params.name === "create_github_issue") { 16 const { owner, repo, title, body, token } = request.params.arguments as any; 17 18 const response = await fetch( 19 `https://api.github.com/repos/${owner}/${repo}/issues`, 20 { 21 method: "POST", 22 headers: { 23 "Authorization": `token ${token}`, 24 "Content-Type": "application/json" 25 }, 26 body: JSON.stringify({ title, body }), 27 signal: AbortSignal.timeout(30000) 28 } 29 ); 30 31 if (!response.ok) { 32 throw new Error(`GitHub API error: ${response.statusText}`); 33 } 34 35 const data = await response.json(); 36 return { 37 content: [{ type: "text", text: JSON.stringify(data) }] 38 }; 39 } 40}); 41 42async function main() { 43 const transport = new StdioServerTransport(); 44 await server.connect(transport); 45} 46 47main().catch(console.error);

Database Connections

FastMCP Example:

python
1import asyncpg 2from fastmcp import FastMCP 3from contextlib import asynccontextmanager 4 5mcp = FastMCP("Database MCP Server") 6pool: asyncpg.Pool = None 7 8@asynccontextmanager 9async def lifespan(app): 10 global pool 11 pool = await asyncpg.create_pool( 12 "postgresql://user:pass@localhost/db", 13 min_size=2, 14 max_size=10 15 ) 16 yield 17 await pool.close() 18 19mcp.lifespan = lifespan 20 21@mcp.tool() 22async def query_users(limit: int = 10) -> list[dict]: 23 """Query users from database""" 24 async with pool.acquire() as conn: 25 rows = await conn.fetch( 26 "SELECT id, name, email FROM users LIMIT $1", 27 limit 28 ) 29 return [dict(row) for row in rows]

MCP SDK Example:

typescript
1import { Pool } from "pg"; 2import { promisify } from "util"; 3 4const pool = new Pool({ 5 connectionString: "postgresql://user:pass@localhost/db", 6 min: 2, 7 max: 10 8}); 9 10server.setRequestHandler(CallToolRequestSchema, async (request) => { 11 if (request.params.name === "query_users") { 12 const { limit = 10 } = request.params.arguments as any; 13 const client = await pool.connect(); 14 try { 15 const result = await client.query( 16 "SELECT id, name, email FROM users LIMIT $1", 17 [limit] 18 ); 19 return { 20 content: [{ 21 type: "text", 22 text: JSON.stringify(result.rows) 23 }] 24 }; 25 } finally { 26 client.release(); 27 } 28 } 29});

File System Operations

FastMCP Example:

python
1from pathlib import Path 2from fastmcp import FastMCP 3 4mcp = FastMCP("File System MCP Server") 5 6@mcp.tool() 7async def read_file(file_path: str) -> dict: 8 """Read file contents safely""" 9 path = Path(file_path).resolve() 10 11 # Security: Prevent directory traversal 12 if not str(path).startswith(str(Path.cwd())): 13 raise ValueError("Access denied: path outside workspace") 14 15 if not path.exists(): 16 raise FileNotFoundError(f"File not found: {file_path}") 17 18 if not path.is_file(): 19 raise ValueError(f"Path is not a file: {file_path}") 20 21 return { 22 "content": path.read_text(), 23 "size": path.stat().st_size 24 }

MCP SDK Example:

typescript
1import * as fs from "fs/promises"; 2import * as path from "path"; 3 4server.setRequestHandler(CallToolRequestSchema, async (request) => { 5 if (request.params.name === "read_file") { 6 const { file_path } = request.params.arguments as any; 7 const resolved = path.resolve(file_path); 8 const cwd = process.cwd(); 9 10 // Security: Prevent directory traversal 11 if (!resolved.startsWith(cwd)) { 12 throw new Error("Access denied: path outside workspace"); 13 } 14 15 const stats = await fs.stat(resolved); 16 if (!stats.isFile()) { 17 throw new Error("Path is not a file"); 18 } 19 20 const content = await fs.readFile(resolved, "utf-8"); 21 return { 22 content: [{ 23 type: "text", 24 text: JSON.stringify({ content, size: stats.size }) 25 }] 26 }; 27 } 28});

Authentication Flows

FastMCP Example:

python
1import os 2from fastmcp import FastMCP 3from typing import Optional 4 5mcp = FastMCP("Authenticated API Server") 6 7class AuthManager: 8 def __init__(self): 9 self._token: Optional[str] = None 10 11 def get_token(self) -> str: 12 if not self._token: 13 self._token = os.getenv("API_TOKEN") 14 if not self._token: 15 raise ValueError("API_TOKEN environment variable not set") 16 return self._token 17 18 def refresh_token(self): 19 self._token = None 20 21auth = AuthManager() 22 23@mcp.tool() 24async def make_authenticated_request(endpoint: str) -> dict: 25 """Make authenticated API request""" 26 token = auth.get_token() 27 # Use token in request 28 pass

MCP SDK Example:

typescript
1class AuthManager { 2 private token: string | null = null; 3 4 getToken(): string { 5 if (!this.token) { 6 this.token = process.env.API_TOKEN || null; 7 if (!this.token) { 8 throw new Error("API_TOKEN environment variable not set"); 9 } 10 } 11 return this.token; 12 } 13 14 refreshToken(): void { 15 this.token = null; 16 } 17} 18 19const auth = new AuthManager();

Proper Async/Await Handling

✅ GOOD: Proper Async Patterns

python
1# FastMCP 2import asyncio 3import httpx 4 5@mcp.tool() 6async def fetch_multiple_resources(urls: list[str]) -> list[dict]: 7 """Fetch multiple resources concurrently""" 8 async with httpx.AsyncClient() as client: 9 tasks = [client.get(url, timeout=10.0) for url in urls] 10 responses = await asyncio.gather(*tasks, return_exceptions=True) 11 12 results = [] 13 for response in responses: 14 if isinstance(response, Exception): 15 results.append({"error": str(response)}) 16 else: 17 response.raise_for_status() 18 results.append(response.json()) 19 return results
typescript
1// MCP SDK 2async function fetchMultipleResources(urls: string[]): Promise<any[]> { 3 const promises = urls.map(url => 4 fetch(url, { signal: AbortSignal.timeout(10000) }) 5 .then(r => r.ok ? r.json() : { error: r.statusText }) 6 .catch(err => ({ error: err.message })) 7 ); 8 return Promise.all(promises); 9}

Error Handling

Explicit Error Types

FastMCP Example:

python
1class MCPError(Exception): 2 """Base MCP error""" 3 pass 4 5class ValidationError(MCPError): 6 """Input validation error""" 7 pass 8 9class APIError(MCPError): 10 """External API error""" 11 def __init__(self, message: str, status_code: int = None): 12 super().__init__(message) 13 self.status_code = status_code 14 15class RateLimitError(APIError): 16 """Rate limit exceeded""" 17 pass 18 19@mcp.tool() 20async def api_call(endpoint: str) -> dict: 21 """Make API call with proper error handling""" 22 try: 23 # API call logic 24 pass 25 except httpx.HTTPStatusError as e: 26 if e.response.status_code == 429: 27 raise RateLimitError("Rate limit exceeded", 429) 28 raise APIError(f"API error: {e.response.status_code}", e.response.status_code) 29 except httpx.RequestError as e: 30 raise APIError(f"Request failed: {str(e)}")

MCP SDK Example:

typescript
1class MCPError extends Error { 2 constructor(message: string, public statusCode?: number) { 3 super(message); 4 this.name = "MCPError"; 5 } 6} 7 8class ValidationError extends MCPError {} 9class APIError extends MCPError {} 10class RateLimitError extends APIError {} 11 12server.setRequestHandler(CallToolRequestSchema, async (request) => { 13 try { 14 // Tool logic 15 } catch (error: any) { 16 if (error.response?.status === 429) { 17 throw new RateLimitError("Rate limit exceeded", 429); 18 } 19 if (error.response) { 20 throw new APIError(`API error: ${error.response.status}`, error.response.status); 21 } 22 throw new MCPError(`Request failed: ${error.message}`); 23 } 24});

User-Facing Error Messages

✅ GOOD: Clear, Actionable Messages

python
1@mcp.tool() 2async def create_issue(title: str, repo: str) -> dict: 3 """Create GitHub issue""" 4 if not title.strip(): 5 raise ValueError( 6 "Issue title cannot be empty. Please provide a descriptive title." 7 ) 8 9 if "/" not in repo: 10 raise ValueError( 11 f"Invalid repository format: '{repo}'. Expected format: 'owner/repo-name'" 12 )

❌ BAD: Technical, Unhelpful

python
1# BAD 2raise ValueError("Invalid input") 3raise Exception("Error occurred")

Retry Logic

FastMCP Example:

python
1import asyncio 2from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type 3 4@retry( 5 stop=stop_after_attempt(3), 6 wait=wait_exponential(multiplier=1, min=2, max=10), 7 retry=retry_if_exception_type((httpx.RequestError, RateLimitError)) 8) 9async def api_call_with_retry(url: str) -> dict: 10 """API call with exponential backoff retry""" 11 async with httpx.AsyncClient() as client: 12 response = await client.get(url, timeout=30.0) 13 if response.status_code == 429: 14 raise RateLimitError("Rate limited") 15 response.raise_for_status() 16 return response.json()

MCP SDK Example:

typescript
1async function retry<T>( 2 fn: () => Promise<T>, 3 maxAttempts: number = 3, 4 delay: number = 1000 5): Promise<T> { 6 for (let i = 0; i < maxAttempts; i++) { 7 try { 8 return await fn(); 9 } catch (error) { 10 if (i === maxAttempts - 1) throw error; 11 if (error instanceof RateLimitError) { 12 await new Promise(r => setTimeout(r, delay * Math.pow(2, i))); 13 } else { 14 throw error; 15 } 16 } 17 } 18 throw new Error("Retry failed"); 19}

Timeout Handling

FastMCP Example:

python
1import asyncio 2 3@mcp.tool() 4async def long_running_operation(timeout_seconds: int = 30) -> dict: 5 """Operation with timeout""" 6 try: 7 return await asyncio.wait_for( 8 perform_operation(), 9 timeout=timeout_seconds 10 ) 11 except asyncio.TimeoutError: 12 raise TimeoutError( 13 f"Operation timed out after {timeout_seconds} seconds. " 14 "Please try again or reduce the scope of the operation." 15 )

MCP SDK Example:

typescript
1async function withTimeout<T>( 2 promise: Promise<T>, 3 timeoutMs: number 4): Promise<T> { 5 return Promise.race([ 6 promise, 7 new Promise<T>((_, reject) => 8 setTimeout(() => reject(new Error(`Timeout after ${timeoutMs}ms`)), timeoutMs) 9 ) 10 ]); 11}

Graceful Degradation

FastMCP Example:

python
1@mcp.tool() 2async def fetch_with_fallback(primary_url: str, fallback_url: str) -> dict: 3 """Fetch with fallback on failure""" 4 try: 5 async with httpx.AsyncClient() as client: 6 response = await client.get(primary_url, timeout=10.0) 7 response.raise_for_status() 8 return response.json() 9 except Exception as e: 10 logger.warning(f"Primary source failed: {e}, trying fallback") 11 try: 12 async with httpx.AsyncClient() as client: 13 response = await client.get(fallback_url, timeout=10.0) 14 response.raise_for_status() 15 return response.json() 16 except Exception as fallback_error: 17 raise APIError( 18 f"Both primary and fallback sources failed. " 19 f"Primary: {str(e)}, Fallback: {str(fallback_error)}" 20 )

Logging Strategies

FastMCP Example:

python
1import logging 2import sys 3 4logging.basicConfig( 5 level=logging.INFO, 6 format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 7 handlers=[ 8 logging.FileHandler("mcp-server.log"), 9 logging.StreamHandler(sys.stderr) 10 ] 11) 12 13logger = logging.getLogger("mcp_server") 14 15@mcp.tool() 16async def tool_with_logging(param: str) -> dict: 17 """Tool with structured logging""" 18 logger.info(f"Tool called with param: {param[:50]}") # Truncate for security 19 try: 20 result = await perform_operation(param) 21 logger.info(f"Tool completed successfully") 22 return result 23 except Exception as e: 24 logger.error(f"Tool failed: {str(e)}", exc_info=True) 25 raise

MCP SDK Example:

typescript
1import * as winston from "winston"; 2 3const logger = winston.createLogger({ 4 level: "info", 5 format: winston.format.combine( 6 winston.format.timestamp(), 7 winston.format.json() 8 ), 9 transports: [ 10 new winston.transports.File({ filename: "mcp-server.log" }), 11 new winston.transports.Console() 12 ] 13}); 14 15server.setRequestHandler(CallToolRequestSchema, async (request) => { 16 logger.info("Tool called", { tool: request.params.name }); 17 try { 18 // Tool logic 19 logger.info("Tool completed"); 20 } catch (error) { 21 logger.error("Tool failed", { error: error.message, stack: error.stack }); 22 throw error; 23 } 24});

Testing Strategy

Unit Tests for Individual Tools

FastMCP Example:

python
1import pytest 2from unittest.mock import AsyncMock, patch 3from mcp_server import create_issue 4 5@pytest.mark.asyncio 6async def test_create_issue_success(): 7 """Test successful issue creation""" 8 with patch("httpx.AsyncClient") as mock_client: 9 mock_response = AsyncMock() 10 mock_response.json.return_value = {"id": 123, "title": "Test"} 11 mock_response.raise_for_status = AsyncMock() 12 13 mock_client.return_value.__aenter__.return_value.post.return_value = mock_response 14 15 result = await create_issue( 16 owner="test", 17 repo="repo", 18 title="Test Issue", 19 body="Body", 20 token="token" 21 ) 22 23 assert result["id"] == 123 24 assert result["title"] == "Test" 25 26@pytest.mark.asyncio 27async def test_create_issue_validation_error(): 28 """Test validation error handling""" 29 with pytest.raises(ValueError, match="title cannot be empty"): 30 await create_issue("test", "repo", "", "body", "token")

MCP SDK Example:

typescript
1import { describe, it, expect, vi } from "vitest"; 2import { createIssue } from "./tools"; 3 4describe("createIssue", () => { 5 it("should create issue successfully", async () => { 6 global.fetch = vi.fn().mockResolvedValue({ 7 ok: true, 8 json: async () => ({ id: 123, title: "Test" }) 9 }); 10 11 const result = await createIssue({ 12 owner: "test", 13 repo: "repo", 14 title: "Test Issue", 15 body: "Body", 16 token: "token" 17 }); 18 19 expect(result.id).toBe(123); 20 }); 21 22 it("should throw validation error for empty title", async () => { 23 await expect( 24 createIssue({ owner: "test", repo: "repo", title: "", body: "body", token: "token" }) 25 ).rejects.toThrow("title cannot be empty"); 26 }); 27});

Integration Tests with Mock Servers

FastMCP Example:

python
1import pytest 2from httpx import ASGITransport, AsyncClient 3from mcp_server import app 4 5@pytest.mark.asyncio 6async def test_integration_create_issue(): 7 """Integration test with mock server""" 8 async with AsyncClient( 9 transport=ASGITransport(app=app), 10 base_url="http://test" 11 ) as client: 12 response = await client.post( 13 "/tools/call", 14 json={ 15 "name": "create_issue", 16 "arguments": { 17 "owner": "test", 18 "repo": "repo", 19 "title": "Test", 20 "body": "Body" 21 } 22 } 23 ) 24 assert response.status_code == 200 25 data = response.json() 26 assert "id" in data

MCP SDK Example:

typescript
1import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 2import { TestTransport } from "./test-transport"; 3 4describe("Integration Tests", () => { 5 it("should handle tool call end-to-end", async () => { 6 const transport = new TestTransport(); 7 await server.connect(transport); 8 9 const response = await transport.sendRequest({ 10 jsonrpc: "2.0", 11 id: 1, 12 method: "tools/call", 13 params: { 14 name: "create_issue", 15 arguments: { 16 owner: "test", 17 repo: "repo", 18 title: "Test", 19 body: "Body" 20 } 21 } 22 }); 23 24 expect(response.result).toHaveProperty("content"); 25 }); 26});

Validation of Tool Schemas

FastMCP Example:

python
1import pytest 2from pydantic import ValidationError 3from mcp_server import CreateIssueInput 4 5def test_tool_schema_validation(): 6 """Test tool input schema validation""" 7 # Valid input 8 valid = CreateIssueInput( 9 title="Test", 10 body="Body", 11 repo="owner/repo" 12 ) 13 assert valid.title == "Test" 14 15 # Invalid input 16 with pytest.raises(ValidationError): 17 CreateIssueInput(title="", body="Body", repo="owner/repo") 18 19 # Invalid repo format 20 with pytest.raises(ValidationError): 21 CreateIssueInput(title="Test", body="Body", repo="invalid")

MCP SDK Example:

typescript
1import { describe, it, expect } from "vitest"; 2import { CreateIssueSchema } from "./schemas"; 3 4describe("Tool Schema Validation", () => { 5 it("should validate correct input", () => { 6 const result = CreateIssueSchema.parse({ 7 title: "Test", 8 body: "Body", 9 repo: "owner/repo" 10 }); 11 expect(result.title).toBe("Test"); 12 }); 13 14 it("should reject invalid input", () => { 15 expect(() => { 16 CreateIssueSchema.parse({ title: "", body: "Body", repo: "owner/repo" }); 17 }).toThrow(); 18 }); 19});

Testing Authentication Flows

FastMCP Example:

python
1@pytest.mark.asyncio 2async def test_authentication_success(): 3 """Test successful authentication""" 4 with patch.dict(os.environ, {"API_TOKEN": "test-token"}): 5 auth = AuthManager() 6 token = auth.get_token() 7 assert token == "test-token" 8 9@pytest.mark.asyncio 10async def test_authentication_missing_token(): 11 """Test missing token error""" 12 with patch.dict(os.environ, {}, clear=True): 13 auth = AuthManager() 14 with pytest.raises(ValueError, match="API_TOKEN"): 15 auth.get_token()

Handling Rate Limits in Tests

FastMCP Example:

python
1@pytest.mark.asyncio 2async def test_rate_limit_handling(): 3 """Test rate limit retry logic""" 4 call_count = 0 5 6 async def mock_api_call(): 7 nonlocal call_count 8 call_count += 1 9 if call_count < 3: 10 raise RateLimitError("Rate limited", 429) 11 return {"success": True} 12 13 result = await api_call_with_retry("http://test.com") 14 assert result["success"] is True 15 assert call_count == 3

Security Best Practices

API Key Management

✅ GOOD: Environment Variables

python
1# FastMCP 2import os 3from fastmcp import FastMCP 4 5mcp = FastMCP("Secure Server") 6 7# Never hardcode keys 8API_KEY = os.getenv("API_KEY") 9if not API_KEY: 10 raise ValueError("API_KEY environment variable must be set") 11 12@mcp.tool() 13async def secure_api_call() -> dict: 14 """Make secure API call""" 15 # Use environment variable 16 headers = {"Authorization": f"Bearer {API_KEY}"} 17 # ...
typescript
1// MCP SDK 2const API_KEY = process.env.API_KEY; 3if (!API_KEY) { 4 throw new Error("API_KEY environment variable must be set"); 5} 6 7// Use API_KEY in requests

❌ BAD: Hardcoded Secrets

python
1# NEVER DO THIS 2API_KEY = "sk-1234567890abcdef"

Input Sanitization

FastMCP Example:

python
1import html 2import re 3from pydantic import validator 4 5class SafeInput(BaseModel): 6 user_input: str 7 8 @validator("user_input") 9 def sanitize_input(cls, v): 10 # Remove potentially dangerous characters 11 v = html.escape(v) 12 # Remove script tags 13 v = re.sub(r"<script[^>]*>.*?</script>", "", v, flags=re.IGNORECASE | re.DOTALL) 14 # Limit length 15 if len(v) > 10000: 16 raise ValueError("Input too long") 17 return v.strip()

MCP SDK Example:

typescript
1import { z } from "zod"; 2import DOMPurify from "isomorphic-dompurify"; 3 4const SafeInputSchema = z.object({ 5 userInput: z.string() 6 .max(10000, "Input too long") 7 .transform(val => DOMPurify.sanitize(val.trim())) 8});

Preventing Injection Attacks

✅ GOOD: Parameterized Queries

python
1# FastMCP - Database 2async def safe_query(user_id: str) -> list[dict]: 3 """Safe database query with parameterization""" 4 async with pool.acquire() as conn: 5 # Use parameterized query 6 rows = await conn.fetch( 7 "SELECT * FROM users WHERE id = $1", 8 user_id # Safe: parameterized 9 ) 10 return [dict(row) for row in rows]

❌ BAD: String Concatenation

python
1# NEVER DO THIS - SQL Injection risk 2query = f"SELECT * FROM users WHERE id = '{user_id}'"

Rate Limiting

FastMCP Example:

python
1from collections import defaultdict 2from datetime import datetime, timedelta 3import asyncio 4 5class RateLimiter: 6 def __init__(self, max_requests: int, window_seconds: int): 7 self.max_requests = max_requests 8 self.window_seconds = window_seconds 9 self.requests: dict[str, list[datetime]] = defaultdict(list) 10 self.lock = asyncio.Lock() 11 12 async def check_rate_limit(self, key: str) -> bool: 13 async with self.lock: 14 now = datetime.now() 15 window_start = now - timedelta(seconds=self.window_seconds) 16 17 # Clean old requests 18 self.requests[key] = [ 19 req_time for req_time in self.requests[key] 20 if req_time > window_start 21 ] 22 23 if len(self.requests[key]) >= self.max_requests: 24 return False 25 26 self.requests[key].append(now) 27 return True 28 29rate_limiter = RateLimiter(max_requests=100, window_seconds=60) 30 31@mcp.tool() 32async def rate_limited_operation() -> dict: 33 """Operation with rate limiting""" 34 if not await rate_limiter.check_rate_limit("default"): 35 raise RateLimitError("Rate limit exceeded. Please try again later.") 36 # Proceed with operation

MCP SDK Example:

typescript
1class RateLimiter { 2 private requests: Map<string, number[]> = new Map(); 3 4 constructor( 5 private maxRequests: number, 6 private windowMs: number 7 ) {} 8 9 check(key: string): boolean { 10 const now = Date.now(); 11 const windowStart = now - this.windowMs; 12 13 const timestamps = this.requests.get(key) || []; 14 const recent = timestamps.filter(ts => ts > windowStart); 15 16 if (recent.length >= this.maxRequests) { 17 return false; 18 } 19 20 recent.push(now); 21 this.requests.set(key, recent); 22 return true; 23 } 24}

Scoping Permissions Appropriately

✅ GOOD: Principle of Least Privilege

python
1# FastMCP - Scoped permissions 2@mcp.tool() 3async def read_user_data(user_id: str, requester_id: str) -> dict: 4 """Read user data with permission check""" 5 # Check if requester has permission 6 if requester_id != user_id: 7 # Check if requester is admin 8 if not await is_admin(requester_id): 9 raise PermissionError("Insufficient permissions") 10 11 return await get_user_data(user_id)

Documentation Standards

Tool Descriptions for LLMs

✅ GOOD: Clear, Action-Oriented Descriptions

python
1# FastMCP 2@mcp.tool() 3async def create_github_issue( 4 owner: str, 5 repo: str, 6 title: str, 7 body: str 8) -> dict: 9 """ 10 Create a new GitHub issue in the specified repository. 11 12 Use this tool when you need to create a bug report, feature request, 13 or any other type of issue in a GitHub repository. The issue will be 14 created with the provided title and body text. 15 16 Args: 17 owner: GitHub username or organization name (e.g., "microsoft") 18 repo: Repository name (e.g., "vscode") 19 title: Issue title (required, 1-200 characters) 20 body: Issue description/body text (required, supports Markdown) 21 22 Returns: 23 Dictionary containing: 24 - id: Issue number 25 - url: URL to the created issue 26 - title: Issue title 27 - state: Issue state (usually "open") 28 29 Raises: 30 ValueError: If title or body is empty 31 APIError: If GitHub API request fails 32 RateLimitError: If GitHub rate limit is exceeded 33 34 Example: 35 >>> create_github_issue( 36 ... owner="microsoft", 37 ... repo="vscode", 38 ... title="Bug: Editor crashes", 39 ... body="The editor crashes when..." 40 ... ) 41 {"id": 12345, "url": "https://github.com/microsoft/vscode/issues/12345", ...} 42 """ 43 pass

❌ BAD: Vague Descriptions

python
1# BAD 2@mcp.tool() 3async def do_stuff(param: str) -> dict: 4 """Does stuff""" 5 pass

Parameter Documentation

FastMCP Example:

python
1from pydantic import Field 2 3@mcp.tool() 4async def search_repositories( 5 query: str = Field( 6 ..., 7 description="Search query string. Supports GitHub search syntax.", 8 examples=["language:python", "stars:>1000"] 9 ), 10 sort: str = Field( 11 default="best-match", 12 description="Sort order: 'best-match', 'stars', 'forks', 'updated'", 13 examples=["stars", "updated"] 14 ), 15 limit: int = Field( 16 default=10, 17 ge=1, 18 le=100, 19 description="Maximum number of results to return (1-100)" 20 ) 21) -> dict: 22 """Search GitHub repositories with detailed parameter docs""" 23 pass

MCP SDK Example:

typescript
1const searchRepositories = { 2 name: "search_repositories", 3 description: "Search GitHub repositories using GitHub's search API", 4 inputSchema: { 5 type: "object", 6 required: ["query"], 7 properties: { 8 query: { 9 type: "string", 10 description: "Search query string. Supports GitHub search syntax. Examples: 'language:python', 'stars:>1000'" 11 }, 12 sort: { 13 type: "string", 14 enum: ["best-match", "stars", "forks", "updated"], 15 default: "best-match", 16 description: "Sort order for results" 17 }, 18 limit: { 19 type: "number", 20 minimum: 1, 21 maximum: 100, 22 default: 10, 23 description: "Maximum number of results to return (1-100)" 24 } 25 } 26 } 27};

Return Value Schemas

FastMCP Example:

python
1from typing import TypedDict 2 3class IssueResult(TypedDict): 4 id: int 5 url: str 6 title: str 7 state: str 8 created_at: str 9 author: str 10 11@mcp.tool() 12async def get_issue(owner: str, repo: str, issue_number: int) -> IssueResult: 13 """ 14 Get GitHub issue details. 15 16 Returns: 17 IssueResult with fields: 18 - id: Issue number 19 - url: Full URL to issue 20 - title: Issue title 21 - state: "open" or "closed" 22 - created_at: ISO 8601 timestamp 23 - author: GitHub username of issue creator 24 """ 25 pass

Usage Examples

FastMCP Example:

python
1@mcp.tool() 2async def create_notion_page( 3 database_id: str, 4 title: str, 5 content: str 6) -> dict: 7 """ 8 Create a new page in a Notion database. 9 10 Examples: 11 Basic usage: 12 >>> create_notion_page( 13 ... database_id="abc123", 14 ... title="Meeting Notes", 15 ... content="# Meeting Notes\\n\\nDiscussion points..." 16 ... ) 17 18 With rich content: 19 >>> create_notion_page( 20 ... database_id="abc123", 21 ... title="Project Plan", 22 ... content="## Phase 1\\n- [ ] Task 1\\n- [ ] Task 2" 23 ... ) 24 """ 25 pass

Common Pitfalls Documentation

FastMCP Example:

python
1@mcp.tool() 2async def update_github_issue( 3 owner: str, 4 repo: str, 5 issue_number: int, 6 title: str = None, 7 body: str = None 8) -> dict: 9 """ 10 Update an existing GitHub issue. 11 12 Common Pitfalls: 13 1. Both title and body are optional, but at least one must be provided 14 2. Issue number must exist in the repository 15 3. You must have write access to the repository 16 4. Rate limits: 5000 requests/hour for authenticated requests 17 18 Error Handling: 19 - 404: Issue not found or repository doesn't exist 20 - 403: Insufficient permissions 21 - 429: Rate limit exceeded (wait and retry) 22 """ 23 if not title and not body: 24 raise ValueError("At least one of 'title' or 'body' must be provided") 25 pass

Production Readiness

Configuration Management

FastMCP Example:

python
1import os 2from pydantic import BaseSettings 3from typing import Optional 4 5class Settings(BaseSettings): 6 api_key: str 7 api_base_url: str = "https://api.example.com" 8 timeout_seconds: int = 30 9 max_retries: int = 3 10 log_level: str = "INFO" 11 database_url: Optional[str] = None 12 13 class Config: 14 env_file = ".env" 15 env_file_encoding = "utf-8" 16 17settings = Settings() 18 19@mcp.tool() 20async def configured_api_call() -> dict: 21 """API call using configuration""" 22 async with httpx.AsyncClient( 23 base_url=settings.api_base_url, 24 timeout=settings.timeout_seconds 25 ) as client: 26 # Use settings.api_key 27 pass

MCP SDK Example:

typescript
1import { config } from "dotenv"; 2config(); 3 4interface Config { 5 apiKey: string; 6 apiBaseUrl: string; 7 timeoutSeconds: number; 8 maxRetries: number; 9 logLevel: string; 10 databaseUrl?: string; 11} 12 13const settings: Config = { 14 apiKey: process.env.API_KEY!, 15 apiBaseUrl: process.env.API_BASE_URL || "https://api.example.com", 16 timeoutSeconds: parseInt(process.env.TIMEOUT_SECONDS || "30"), 17 maxRetries: parseInt(process.env.MAX_RETRIES || "3"), 18 logLevel: process.env.LOG_LEVEL || "INFO", 19 databaseUrl: process.env.DATABASE_URL 20};

Dependency Management

FastMCP - requirements.txt:

txt
1fastmcp>=0.9.0 2httpx>=0.25.0 3pydantic>=2.0.0 4python-dotenv>=1.0.0 5tenacity>=8.2.0

MCP SDK - package.json:

json
1{ 2 "name": "mcp-server", 3 "version": "1.0.0", 4 "dependencies": { 5 "@modelcontextprotocol/sdk": "^0.5.0", 6 "zod": "^3.22.0", 7 "dotenv": "^16.3.0", 8 "node-fetch": "^3.3.0" 9 }, 10 "devDependencies": { 11 "@types/node": "^20.0.0", 12 "typescript": "^5.0.0", 13 "vitest": "^1.0.0" 14 } 15}

Deployment Considerations

Dockerfile Example:

dockerfile
1# FastMCP 2FROM python:3.11-slim 3 4WORKDIR /app 5 6COPY requirements.txt . 7RUN pip install --no-cache-dir -r requirements.txt 8 9COPY . . 10 11CMD ["python", "-m", "mcp_server"]
dockerfile
1# MCP SDK 2FROM node:20-slim 3 4WORKDIR /app 5 6COPY package*.json ./ 7RUN npm ci --only=production 8 9COPY . . 10RUN npm run build 11 12CMD ["node", "dist/index.js"]

Monitoring and Logging

FastMCP Example:

python
1import logging 2import json 3from datetime import datetime 4 5class StructuredLogger: 6 def __init__(self): 7 self.logger = logging.getLogger("mcp_server") 8 handler = logging.StreamHandler() 9 handler.setFormatter(logging.Formatter( 10 '%(message)s' # JSON format 11 )) 12 self.logger.addHandler(handler) 13 self.logger.setLevel(logging.INFO) 14 15 def log_tool_call(self, tool_name: str, duration_ms: float, success: bool): 16 self.logger.info(json.dumps({ 17 "timestamp": datetime.utcnow().isoformat(), 18 "event": "tool_call", 19 "tool": tool_name, 20 "duration_ms": duration_ms, 21 "success": success 22 }))

MCP SDK Example:

typescript
1import * as winston from "winston"; 2 3const logger = winston.createLogger({ 4 format: winston.format.combine( 5 winston.format.timestamp(), 6 winston.format.json() 7 ), 8 transports: [ 9 new winston.transports.Console(), 10 new winston.transports.File({ filename: "mcp-server.log" }) 11 ] 12}); 13 14function logToolCall(toolName: string, durationMs: number, success: boolean) { 15 logger.info("tool_call", { 16 tool: toolName, 17 duration_ms: durationMs, 18 success 19 }); 20}

Versioning Strategy

FastMCP Example:

python
1from fastmcp import FastMCP 2 3mcp = FastMCP( 4 "My MCP Server", 5 version="1.2.3" 6) 7 8# Semantic versioning: MAJOR.MINOR.PATCH 9# MAJOR: Breaking changes 10# MINOR: New features, backwards compatible 11# PATCH: Bug fixes, backwards compatible

MCP SDK Example:

typescript
1const server = new Server({ 2 name: "my-mcp-server", 3 version: "1.2.3" // Semantic versioning 4}, { 5 capabilities: { 6 tools: {} 7 } 8});

Backwards Compatibility

✅ GOOD: Additive Changes

python
1# Version 1.0 2@mcp.tool() 3async def create_issue(title: str, body: str) -> dict: 4 """Create issue""" 5 pass 6 7# Version 1.1 - Backwards compatible (new optional parameter) 8@mcp.tool() 9async def create_issue( 10 title: str, 11 body: str, 12 labels: list[str] = None # New optional parameter 13) -> dict: 14 """Create issue with optional labels""" 15 pass

❌ BAD: Breaking Changes

python
1# Version 1.0 2@mcp.tool() 3async def create_issue(title: str, body: str) -> dict: 4 pass 5 6# Version 2.0 - BREAKING: Changed parameter name 7@mcp.tool() 8async def create_issue(heading: str, content: str) -> dict: # BAD 9 pass

Common Pitfalls

Anti-Patterns to Avoid

1. God Tools (Too Many Responsibilities)

python
1# ❌ BAD 2@mcp.tool() 3async def manage_github(action: str, **kwargs) -> dict: 4 """Do everything GitHub-related""" 5 if action == "create_issue": 6 # ... 7 elif action == "create_pr": 8 # ... 9 elif action == "list_repos": 10 # ... 11 # Too many responsibilities!
python
1# ✅ GOOD 2@mcp.tool() 3async def create_github_issue(...) -> dict: 4 """Create a GitHub issue""" 5 pass 6 7@mcp.tool() 8async def create_github_pr(...) -> dict: 9 """Create a GitHub pull request""" 10 pass 11 12@mcp.tool() 13async def list_github_repos(...) -> dict: 14 """List GitHub repositories""" 15 pass

2. Ignoring Errors

python
1# ❌ BAD 2@mcp.tool() 3async def api_call() -> dict: 4 try: 5 result = await make_api_call() 6 return result 7 except: 8 return {} # Silent failure!
python
1# ✅ GOOD 2@mcp.tool() 3async def api_call() -> dict: 4 try: 5 result = await make_api_call() 6 return result 7 except httpx.HTTPStatusError as e: 8 raise APIError(f"API error: {e.response.status_code}") 9 except Exception as e: 10 logger.error(f"Unexpected error: {e}", exc_info=True) 11 raise

3. No Input Validation

python
1# ❌ BAD 2@mcp.tool() 3async def query_database(sql: str) -> dict: 4 # No validation - SQL injection risk! 5 return await db.execute(sql)
python
1# ✅ GOOD 2@mcp.tool() 3async def query_database(table: str, filters: dict) -> dict: 4 # Validate and use parameterized queries 5 if table not in ALLOWED_TABLES: 6 raise ValueError(f"Table '{table}' not allowed") 7 # Use parameterized query 8 return await db.execute_parameterized(table, filters)

Performance Bottlenecks

1. Synchronous Operations in Async Context

python
1# ❌ BAD 2@mcp.tool() 3async def read_file(file_path: str) -> dict: 4 content = open(file_path).read() # Blocking! 5 return {"content": content}
python
1# ✅ GOOD 2@mcp.tool() 3async def read_file(file_path: str) -> dict: 4 content = await asyncio.to_thread( 5 lambda: Path(file_path).read_text() 6 ) 7 return {"content": content}

2. Not Using Connection Pooling

python
1# ❌ BAD 2@mcp.tool() 3async def db_query() -> dict: 4 conn = await asyncpg.connect(DB_URL) # New connection each time! 5 result = await conn.fetch("SELECT * FROM users") 6 await conn.close() 7 return result
python
1# ✅ GOOD 2pool = await asyncpg.create_pool(DB_URL, min_size=2, max_size=10) 3 4@mcp.tool() 5async def db_query() -> dict: 6 async with pool.acquire() as conn: # Reuse connection 7 result = await conn.fetch("SELECT * FROM users") 8 return result

Common Bugs

1. Race Conditions

python
1# ❌ BAD 2counter = 0 3 4@mcp.tool() 5async def increment() -> dict: 6 global counter 7 counter += 1 # Race condition! 8 return {"count": counter}
python
1# ✅ GOOD 2import asyncio 3 4counter = 0 5lock = asyncio.Lock() 6 7@mcp.tool() 8async def increment() -> dict: 9 global counter 10 async with lock: 11 counter += 1 12 return {"count": counter}

2. Resource Leaks

python
1# ❌ BAD 2@mcp.tool() 3async def api_call() -> dict: 4 client = httpx.AsyncClient() # Never closed! 5 response = await client.get("https://api.example.com") 6 return response.json()
python
1# ✅ GOOD 2@mcp.tool() 3async def api_call() -> dict: 4 async with httpx.AsyncClient() as client: # Auto-closed 5 response = await client.get("https://api.example.com") 6 return response.json()

Overly Complex Tool Designs

❌ BAD: Over-Engineered

python
1@mcp.tool() 2async def complex_operation( 3 config: dict, 4 options: dict, 5 callbacks: list[callable], 6 middleware: list[callable] 7) -> dict: 8 """Too many layers of abstraction""" 9 # Complex nested logic 10 pass

✅ GOOD: Simple and Clear

python
1@mcp.tool() 2async def create_issue(title: str, body: str, repo: str) -> dict: 3 """Create issue - simple and clear""" 4 # Straightforward implementation 5 pass

Poor Error Messages

❌ BAD:

python
1raise ValueError("Error") 2raise Exception("Failed")

✅ GOOD:

python
1raise ValueError( 2 "Issue title cannot be empty. Please provide a descriptive title " 3 "that summarizes the issue (1-200 characters)." 4)

Real-World Examples

GitHub API Integration

Complete FastMCP Example:

python
1import os 2import httpx 3from fastmcp import FastMCP 4from pydantic import BaseModel, Field, HttpUrl 5from typing import Optional 6 7mcp = FastMCP("GitHub MCP Server") 8 9GITHUB_TOKEN = os.getenv("GITHUB_TOKEN") 10if not GITHUB_TOKEN: 11 raise ValueError("GITHUB_TOKEN environment variable required") 12 13class CreateIssueInput(BaseModel): 14 owner: str = Field(..., description="Repository owner (username or org)") 15 repo: str = Field(..., description="Repository name") 16 title: str = Field(..., min_length=1, max_length=200) 17 body: str = Field(..., min_length=1) 18 labels: list[str] = Field(default_factory=list, max_items=20) 19 20class IssueResult(BaseModel): 21 id: int 22 number: int 23 title: str 24 url: str 25 state: str 26 created_at: str 27 28@mcp.tool() 29async def create_github_issue(input: CreateIssueInput) -> IssueResult: 30 """ 31 Create a new GitHub issue in the specified repository. 32 33 Requires GITHUB_TOKEN environment variable with 'repo' scope. 34 """ 35 async with httpx.AsyncClient() as client: 36 response = await client.post( 37 f"https://api.github.com/repos/{input.owner}/{input.repo}/issues", 38 json={ 39 "title": input.title, 40 "body": input.body, 41 "labels": input.labels 42 }, 43 headers={ 44 "Authorization": f"token {GITHUB_TOKEN}", 45 "Accept": "application/vnd.github.v3+json" 46 }, 47 timeout=30.0 48 ) 49 50 if response.status_code == 401: 51 raise ValueError("Invalid GITHUB_TOKEN. Check your token permissions.") 52 elif response.status_code == 403: 53 raise ValueError("Insufficient permissions. Token needs 'repo' scope.") 54 elif response.status_code == 404: 55 raise ValueError(f"Repository {input.owner}/{input.repo} not found.") 56 elif response.status_code == 429: 57 raise ValueError("GitHub rate limit exceeded. Please wait before retrying.") 58 59 response.raise_for_status() 60 data = response.json() 61 62 return IssueResult( 63 id=data["id"], 64 number=data["number"], 65 title=data["title"], 66 url=data["html_url"], 67 state=data["state"], 68 created_at=data["created_at"] 69 ) 70 71@mcp.tool() 72async def list_github_issues( 73 owner: str, 74 repo: str, 75 state: str = "open", 76 limit: int = 10 77) -> list[IssueResult]: 78 """List GitHub issues in a repository""" 79 async with httpx.AsyncClient() as client: 80 response = await client.get( 81 f"https://api.github.com/repos/{owner}/{repo}/issues", 82 params={"state": state, "per_page": min(limit, 100)}, 83 headers={ 84 "Authorization": f"token {GITHUB_TOKEN}", 85 "Accept": "application/vnd.github.v3+json" 86 }, 87 timeout=30.0 88 ) 89 response.raise_for_status() 90 return [IssueResult(**issue) for issue in response.json()] 91 92if __name__ == "__main__": 93 mcp.run()

Complete MCP SDK Example:

typescript
1import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 2import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 3import { 4 CallToolRequestSchema, 5 ListToolsRequestSchema 6} from "@modelcontextprotocol/sdk/types.js"; 7import fetch from "node-fetch"; 8import { z } from "zod"; 9 10const GITHUB_TOKEN = process.env.GITHUB_TOKEN; 11if (!GITHUB_TOKEN) { 12 throw new Error("GITHUB_TOKEN environment variable required"); 13} 14 15const CreateIssueSchema = z.object({ 16 owner: z.string().describe("Repository owner (username or org)"), 17 repo: z.string().describe("Repository name"), 18 title: z.string().min(1).max(200), 19 body: z.string().min(1), 20 labels: z.array(z.string()).max(20).default([]) 21}); 22 23const server = new Server({ 24 name: "github-mcp-server", 25 version: "1.0.0" 26}, { 27 capabilities: { 28 tools: {} 29 } 30}); 31 32server.setRequestHandler(ListToolsRequestSchema, async () => ({ 33 tools: [ 34 { 35 name: "create_github_issue", 36 description: "Create a new GitHub issue in the specified repository. Requires GITHUB_TOKEN with 'repo' scope.", 37 inputSchema: { 38 type: "object", 39 required: ["owner", "repo", "title", "body"], 40 properties: { 41 owner: { type: "string", description: "Repository owner" }, 42 repo: { type: "string", description: "Repository name" }, 43 title: { type: "string", minLength: 1, maxLength: 200 }, 44 body: { type: "string", minLength: 1 }, 45 labels: { 46 type: "array", 47 items: { type: "string" }, 48 maxItems: 20, 49 default: [] 50 } 51 } 52 } 53 }, 54 { 55 name: "list_github_issues", 56 description: "List GitHub issues in a repository", 57 inputSchema: { 58 type: "object", 59 required: ["owner", "repo"], 60 properties: { 61 owner: { type: "string" }, 62 repo: { type: "string" }, 63 state: { type: "string", enum: ["open", "closed", "all"], default: "open" }, 64 limit: { type: "number", minimum: 1, maximum: 100, default: 10 } 65 } 66 } 67 } 68 ] 69})); 70 71server.setRequestHandler(CallToolRequestSchema, async (request) => { 72 if (request.params.name === "create_github_issue") { 73 const input = CreateIssueSchema.parse(request.params.arguments); 74 75 const response = await fetch( 76 `https://api.github.com/repos/${input.owner}/${input.repo}/issues`, 77 { 78 method: "POST", 79 headers: { 80 "Authorization": `token ${GITHUB_TOKEN}`, 81 "Accept": "application/vnd.github.v3+json", 82 "Content-Type": "application/json" 83 }, 84 body: JSON.stringify({ 85 title: input.title, 86 body: input.body, 87 labels: input.labels 88 }), 89 signal: AbortSignal.timeout(30000) 90 } 91 ); 92 93 if (response.status === 401) { 94 throw new Error("Invalid GITHUB_TOKEN. Check your token permissions."); 95 } else if (response.status === 403) { 96 throw new Error("Insufficient permissions. Token needs 'repo' scope."); 97 } else if (response.status === 404) { 98 throw new Error(`Repository ${input.owner}/${input.repo} not found.`); 99 } else if (response.status === 429) { 100 throw new Error("GitHub rate limit exceeded. Please wait before retrying."); 101 } 102 103 if (!response.ok) { 104 throw new Error(`GitHub API error: ${response.statusText}`); 105 } 106 107 const data = await response.json(); 108 return { 109 content: [{ 110 type: "text", 111 text: JSON.stringify({ 112 id: data.id, 113 number: data.number, 114 title: data.title, 115 url: data.html_url, 116 state: data.state, 117 created_at: data.created_at 118 }) 119 }] 120 }; 121 } 122 123 if (request.params.name === "list_github_issues") { 124 const { owner, repo, state = "open", limit = 10 } = request.params.arguments as any; 125 126 const response = await fetch( 127 `https://api.github.com/repos/${owner}/${repo}/issues?state=${state}&per_page=${Math.min(limit, 100)}`, 128 { 129 headers: { 130 "Authorization": `token ${GITHUB_TOKEN}`, 131 "Accept": "application/vnd.github.v3+json" 132 }, 133 signal: AbortSignal.timeout(30000) 134 } 135 ); 136 137 if (!response.ok) { 138 throw new Error(`GitHub API error: ${response.statusText}`); 139 } 140 141 const issues = await response.json(); 142 return { 143 content: [{ 144 type: "text", 145 text: JSON.stringify(issues.map((issue: any) => ({ 146 id: issue.id, 147 number: issue.number, 148 title: issue.title, 149 url: issue.html_url, 150 state: issue.state, 151 created_at: issue.created_at 152 }))) 153 }] 154 }; 155 } 156 157 throw new Error(`Unknown tool: ${request.params.name}`); 158}); 159 160async function main() { 161 const transport = new StdioServerTransport(); 162 await server.connect(transport); 163 console.error("GitHub MCP server running on stdio"); 164} 165 166main().catch(console.error);

Notion API Integration

FastMCP Example:

python
1import os 2import httpx 3from fastmcp import FastMCP 4from pydantic import BaseModel 5from typing import Optional 6 7mcp = FastMCP("Notion MCP Server") 8 9NOTION_TOKEN = os.getenv("NOTION_TOKEN") 10if not NOTION_TOKEN: 11 raise ValueError("NOTION_TOKEN environment variable required") 12 13class CreatePageInput(BaseModel): 14 database_id: str 15 title: str 16 content: Optional[str] = None 17 18@mcp.tool() 19async def create_notion_page(input: CreatePageInput) -> dict: 20 """Create a new page in a Notion database""" 21 async with httpx.AsyncClient() as client: 22 response = await client.post( 23 "https://api.notion.com/v1/pages", 24 json={ 25 "parent": {"database_id": input.database_id}, 26 "properties": { 27 "Title": { 28 "title": [{"text": {"content": input.title}}] 29 } 30 }, 31 "children": [ 32 { 33 "object": "block", 34 "type": "paragraph", 35 "paragraph": { 36 "rich_text": [{"text": {"content": input.content or ""}}] 37 } 38 } 39 ] if input.content else [] 40 }, 41 headers={ 42 "Authorization": f"Bearer {NOTION_TOKEN}", 43 "Notion-Version": "2022-06-28", 44 "Content-Type": "application/json" 45 }, 46 timeout=30.0 47 ) 48 49 if response.status_code == 401: 50 raise ValueError("Invalid NOTION_TOKEN") 51 elif response.status_code == 404: 52 raise ValueError(f"Database {input.database_id} not found") 53 54 response.raise_for_status() 55 return response.json()

MCP SDK Example:

typescript
1const NOTION_TOKEN = process.env.NOTION_TOKEN; 2if (!NOTION_TOKEN) { 3 throw new Error("NOTION_TOKEN environment variable required"); 4} 5 6server.setRequestHandler(CallToolRequestSchema, async (request) => { 7 if (request.params.name === "create_notion_page") { 8 const { database_id, title, content } = request.params.arguments as any; 9 10 const response = await fetch("https://api.notion.com/v1/pages", { 11 method: "POST", 12 headers: { 13 "Authorization": `Bearer ${NOTION_TOKEN}`, 14 "Notion-Version": "2022-06-28", 15 "Content-Type": "application/json" 16 }, 17 body: JSON.stringify({ 18 parent: { database_id }, 19 properties: { 20 Title: { 21 title: [{ text: { content: title } }] 22 } 23 }, 24 children: content ? [{ 25 object: "block", 26 type: "paragraph", 27 paragraph: { 28 rich_text: [{ text: { content } }] 29 } 30 }] : [] 31 }), 32 signal: AbortSignal.timeout(30000) 33 }); 34 35 if (!response.ok) { 36 throw new Error(`Notion API error: ${response.statusText}`); 37 } 38 39 const data = await response.json(); 40 return { 41 content: [{ type: "text", text: JSON.stringify(data) }] 42 }; 43 } 44});

Database Connection Example

FastMCP Example:

python
1import asyncpg 2from fastmcp import FastMCP 3from contextlib import asynccontextmanager 4import os 5 6mcp = FastMCP("Database MCP Server") 7 8DATABASE_URL = os.getenv("DATABASE_URL") 9if not DATABASE_URL: 10 raise ValueError("DATABASE_URL environment variable required") 11 12pool: asyncpg.Pool = None 13 14@asynccontextmanager 15async def lifespan(app): 16 global pool 17 pool = await asyncpg.create_pool( 18 DATABASE_URL, 19 min_size=2, 20 max_size=10, 21 command_timeout=30 22 ) 23 yield 24 await pool.close() 25 26mcp.lifespan = lifespan 27 28@mcp.tool() 29async def query_users(limit: int = 10, offset: int = 0) -> list[dict]: 30 """Query users from database with pagination""" 31 async with pool.acquire() as conn: 32 rows = await conn.fetch( 33 "SELECT id, name, email, created_at FROM users ORDER BY created_at DESC LIMIT $1 OFFSET $2", 34 limit, 35 offset 36 ) 37 return [dict(row) for row in rows] 38 39@mcp.tool() 40async def create_user(name: str, email: str) -> dict: 41 """Create a new user in the database""" 42 async with pool.acquire() as conn: 43 row = await conn.fetchrow( 44 "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email, created_at", 45 name, 46 email 47 ) 48 return dict(row)

MCP SDK Example:

typescript
1import { Pool } from "pg"; 2 3const pool = new Pool({ 4 connectionString: process.env.DATABASE_URL, 5 min: 2, 6 max: 10, 7 connectionTimeoutMillis: 30000 8}); 9 10server.setRequestHandler(CallToolRequestSchema, async (request) => { 11 if (request.params.name === "query_users") { 12 const { limit = 10, offset = 0 } = request.params.arguments as any; 13 const client = await pool.connect(); 14 try { 15 const result = await client.query( 16 "SELECT id, name, email, created_at FROM users ORDER BY created_at DESC LIMIT $1 OFFSET $2", 17 [limit, offset] 18 ); 19 return { 20 content: [{ type: "text", text: JSON.stringify(result.rows) }] 21 }; 22 } finally { 23 client.release(); 24 } 25 } 26 27 if (request.params.name === "create_user") { 28 const { name, email } = request.params.arguments as any; 29 const client = await pool.connect(); 30 try { 31 const result = await client.query( 32 "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email, created_at", 33 [name, email] 34 ); 35 return { 36 content: [{ type: "text", text: JSON.stringify(result.rows[0]) }] 37 }; 38 } finally { 39 client.release(); 40 } 41 } 42});

File Operations Example

FastMCP Example:

python
1from pathlib import Path 2from fastmcp import FastMCP 3from pydantic import BaseModel, Field 4import os 5 6mcp = FastMCP("File System MCP Server") 7 8WORKSPACE_ROOT = Path(os.getenv("WORKSPACE_ROOT", ".")).resolve() 9 10class ReadFileInput(BaseModel): 11 file_path: str = Field(..., description="Relative path from workspace root") 12 13def validate_path(file_path: str) -> Path: 14 """Validate and resolve file path safely""" 15 resolved = (WORKSPACE_ROOT / file_path).resolve() 16 17 # Security: Prevent directory traversal 18 if not str(resolved).startswith(str(WORKSPACE_ROOT)): 19 raise ValueError(f"Access denied: path outside workspace") 20 21 if not resolved.exists(): 22 raise FileNotFoundError(f"File not found: {file_path}") 23 24 if not resolved.is_file(): 25 raise ValueError(f"Path is not a file: {file_path}") 26 27 return resolved 28 29@mcp.tool() 30async def read_file(input: ReadFileInput) -> dict: 31 """Read file contents safely""" 32 path = validate_path(input.file_path) 33 return { 34 "content": path.read_text(), 35 "size": path.stat().st_size, 36 "path": str(path.relative_to(WORKSPACE_ROOT)) 37 } 38 39@mcp.tool() 40async def write_file(file_path: str, content: str) -> dict: 41 """Write file contents safely""" 42 path = validate_path(file_path) 43 path.write_text(content) 44 return { 45 "success": True, 46 "path": str(path.relative_to(WORKSPACE_ROOT)), 47 "size": len(content) 48 } 49 50@mcp.tool() 51async def list_directory(dir_path: str = ".") -> list[dict]: 52 """List directory contents""" 53 resolved = (WORKSPACE_ROOT / dir_path).resolve() 54 55 if not str(resolved).startswith(str(WORKSPACE_ROOT)): 56 raise ValueError("Access denied: path outside workspace") 57 58 if not resolved.exists(): 59 raise FileNotFoundError(f"Directory not found: {dir_path}") 60 61 if not resolved.is_dir(): 62 raise ValueError(f"Path is not a directory: {dir_path}") 63 64 return [ 65 { 66 "name": item.name, 67 "path": str(item.relative_to(WORKSPACE_ROOT)), 68 "type": "directory" if item.is_dir() else "file", 69 "size": item.stat().st_size if item.is_file() else None 70 } 71 for item in resolved.iterdir() 72 ]

MCP SDK Example:

typescript
1import * as fs from "fs/promises"; 2import * as path from "path"; 3 4const WORKSPACE_ROOT = path.resolve(process.env.WORKSPACE_ROOT || "."); 5 6function validatePath(filePath: string): string { 7 const resolved = path.resolve(WORKSPACE_ROOT, filePath); 8 9 if (!resolved.startsWith(WORKSPACE_ROOT)) { 10 throw new Error("Access denied: path outside workspace"); 11 } 12 13 return resolved; 14} 15 16server.setRequestHandler(CallToolRequestSchema, async (request) => { 17 if (request.params.name === "read_file") { 18 const { file_path } = request.params.arguments as any; 19 const resolved = validatePath(file_path); 20 21 const stats = await fs.stat(resolved); 22 if (!stats.isFile()) { 23 throw new Error("Path is not a file"); 24 } 25 26 const content = await fs.readFile(resolved, "utf-8"); 27 return { 28 content: [{ 29 type: "text", 30 text: JSON.stringify({ 31 content, 32 size: stats.size, 33 path: path.relative(WORKSPACE_ROOT, resolved) 34 }) 35 }] 36 }; 37 } 38 39 if (request.params.name === "write_file") { 40 const { file_path, content } = request.params.arguments as any; 41 const resolved = validatePath(file_path); 42 43 await fs.writeFile(resolved, content, "utf-8"); 44 return { 45 content: [{ 46 type: "text", 47 text: JSON.stringify({ 48 success: true, 49 path: path.relative(WORKSPACE_ROOT, resolved), 50 size: content.length 51 }) 52 }] 53 }; 54 } 55 56 if (request.params.name === "list_directory") { 57 const { dir_path = "." } = request.params.arguments as any; 58 const resolved = validatePath(dir_path); 59 60 const stats = await fs.stat(resolved); 61 if (!stats.isDir()) { 62 throw new Error("Path is not a directory"); 63 } 64 65 const items = await fs.readdir(resolved); 66 const results = await Promise.all( 67 items.map(async (item) => { 68 const itemPath = path.join(resolved, item); 69 const itemStats = await fs.stat(itemPath); 70 return { 71 name: item, 72 path: path.relative(WORKSPACE_ROOT, itemPath), 73 type: itemStats.isDirectory() ? "directory" : "file", 74 size: itemStats.isFile() ? itemStats.size : null 75 }; 76 }) 77 ); 78 79 return { 80 content: [{ type: "text", text: JSON.stringify(results) }] 81 }; 82 } 83});

Implementation Quality Checklist

Use this checklist to validate your MCP server implementation:

Tool Design

  • Each tool has a single, clear responsibility
  • Tool names are descriptive and action-oriented
  • Input parameters are validated with proper types
  • Output schemas are well-defined
  • Tools handle edge cases appropriately

Error Handling

  • All errors are caught and handled appropriately
  • Error messages are user-friendly and actionable
  • Different error types are distinguished
  • Retry logic is implemented where appropriate
  • Timeouts are set for all external calls

Security

  • API keys are stored in environment variables
  • Input is sanitized and validated
  • SQL injection is prevented (parameterized queries)
  • Path traversal is prevented
  • Rate limiting is implemented
  • Permissions are scoped appropriately

Testing

  • Unit tests cover all tools
  • Integration tests validate end-to-end flows
  • Error cases are tested
  • Authentication flows are tested
  • Rate limiting is tested

Documentation

  • Tool descriptions are clear and LLM-friendly
  • Parameters are documented with examples
  • Return value schemas are documented
  • Common pitfalls are documented
  • Usage examples are provided

Production Readiness

  • Configuration is managed via environment variables
  • Dependencies are properly versioned
  • Logging is implemented
  • Monitoring is set up
  • Versioning strategy is defined
  • Backwards compatibility is maintained

Decision Trees

When to Create a New Tool vs Add Parameters

Is it a different operation?
├─ YES → Create new tool
└─ NO → Is it the same operation with different filters/options?
    ├─ YES → Add parameters to existing tool
    └─ NO → Consider if operations are related
        ├─ Related and often used together → Consider combining
        └─ Unrelated → Create separate tools

Error Handling Strategy

Error occurs
├─ Is it a validation error?
│   ├─ YES → Return ValueError with clear message
│   └─ NO → Is it a transient error (network, timeout)?
│       ├─ YES → Implement retry with exponential backoff
│       └─ NO → Is it a permanent error (404, 403)?
│           ├─ YES → Return specific error type with actionable message
│           └─ NO → Log and return generic error

Transport Layer Selection

What is your use case?
├─ Local development/CLI → Use stdio
├─ Web service/remote → Use HTTP
├─ Real-time updates needed → Use SSE
└─ Need bidirectional streaming → Consider HTTP with WebSockets

Conclusion

This guide provides comprehensive, actionable guidance for building production-ready MCP servers. Follow these patterns and principles to create maintainable, secure, and reliable MCP servers that integrate seamlessly with Claude Desktop and other MCP clients.

Remember:

  • Start simple: Begin with atomic, single-purpose tools
  • Validate everything: Input validation prevents security issues
  • Handle errors gracefully: User-friendly error messages improve UX
  • Test thoroughly: Comprehensive tests catch issues early
  • Document clearly: Good documentation helps LLMs use your tools effectively
  • Plan for production: Consider monitoring, logging, and deployment from the start

For additional resources:

Related Skills

Looking for an alternative to MCP Builder or building a Categories.community AI Agent? Explore these related open-source MCP Servers.

View All

widget-generator

Logo of f
f

widget-generator is an open-source AI agent skill for creating widget plugins that are injected into prompt feeds on prompts.chat. It supports two rendering modes: standard prompt widgets using default PromptCard styling and custom render widgets built as full React components.

149.6k
0
Design

chat-sdk

Logo of lobehub
lobehub

chat-sdk is a unified TypeScript SDK for building chat bots across multiple platforms, providing a single interface for deploying bot logic.

73.0k
0
Communication

zustand

Logo of lobehub
lobehub

The ultimate space for work and life — to find, build, and collaborate with agent teammates that grow with you. We are taking agent harness to the next level — enabling multi-agent collaboration, effortless agent team design, and introducing agents as the unit of work interaction.

72.8k
0
Communication

data-fetching

Logo of lobehub
lobehub

The ultimate space for work and life — to find, build, and collaborate with agent teammates that grow with you. We are taking agent harness to the next level — enabling multi-agent collaboration, effortless agent team design, and introducing agents as the unit of work interaction.

72.8k
0
Communication